﻿using Sidvall.Business;
using Sidvall.Collections;
using System.Collections.Generic;
using System.Linq;

namespace Sidvall.Data
{
    public class BusinessRuleCollection : System.Collections.ObjectModel.KeyedCollection<string, BusinessRule>, System.Collections.Specialized.INotifyCollectionChanged,
        IBusinessRuleCollectionSettingsProvider
    {
        #region Public Members

        public event System.EventHandler<BusinessRulePropertyEventArgs> BusinessRulePropertyChanged;
        public event System.Collections.Specialized.NotifyCollectionChangedEventHandler CollectionChanged;

        #region Add

        public BusinessRule Add(string ruleGroupName, string ruleName, string description, RuleSeverity severity, bool isBroken)
        {
            return Add(ruleGroupName, ruleName, description, severity, isBroken, null);
        }
        public BusinessRule Add(string ruleGroupName, string ruleName, string description, RuleSeverity severity, bool isBroken, params string[] affectedProperties)
        {
            return Add(ruleGroupName, ruleName, description, severity, isBroken, affectedProperties.AsEnumerable());
        }
        public BusinessRule Add(string ruleGroupName, string ruleName, string description, RuleSeverity severity, bool isBroken, IEnumerable<string> affectedProperties)
        {
            BusinessRule currentItem;

            var key = GetKey(ruleGroupName, ruleName);
            if (key == null)
                return null;
            if (Contains(key))
            {
                currentItem = this[key];
                if (isBroken)
                    currentItem.IsBroken = isBroken;
            }
            else
            {
                currentItem = new BusinessRule()
                {
                    RuleGroupName = ruleGroupName,
                    RuleName = ruleName,
                    Description = description,
                    Severity = severity,
                    IsBroken = isBroken,
                };
                currentItem.AddAffectedProperties(affectedProperties);
                Add(currentItem);
            }
            return currentItem;
        }

        #endregion
        #region AddRange

        public void AddRange(bool includeNotBrokenRules, params BusinessRule[] items)
        {
            AddRange(includeNotBrokenRules, items.AsEnumerable());
        }
        public void AddRange(bool includeNotBrokenRules, IEnumerable<BusinessRule> items)
        {
            BusinessRule currentItem;

            if (items == null)
                return;
            var addedItems = new List<BusinessRule>();
            using (var manager = new BusinessRuleCollectionSettingsManager(this, BusinessRuleCollectionSettings.CreateSettingsNone()))
            {
                foreach (var item in items)
                {
                    if ((item == null) || ((!includeNotBrokenRules) && (!item.IsBroken)))
                        continue;
                    var key = GetKey(item);
                    if (key == null)
                        continue;
                    if (Contains(key))
                    {
                        currentItem = this[key];
                        if (item.IsBroken)
                            currentItem.IsBroken = item.IsBroken;
                    }
                    else
                    {
                        Add(item);
                        addedItems.Add(item);
                    }
                }
            }
            if (addedItems.Count > 0)
                OnCollectionChanged(new System.Collections.Specialized.NotifyCollectionChangedEventArgs(System.Collections.Specialized.NotifyCollectionChangedAction.Add, addedItems));
        }

        #endregion
        #region AddAffectedProperties

        public BusinessRuleCollection AddAffectedProperties(IEnumerable<string> items)
        {
            return AddAffectedProperties(items, null);
        }
        public BusinessRuleCollection AddAffectedProperties(IEnumerable<string> items, System.Func<BusinessRule, bool> includeHandler)
        {
            foreach (var item in this)
            {
                if ((includeHandler == null) || (includeHandler.Invoke(item)))
                    item.AddAffectedProperties(items);
            }
            return this;
        }
        public BusinessRuleCollection AddAffectedProperties(params string[] items)
        {
            return AddAffectedProperties(items.AsEnumerable());
        }

        #endregion
        #region AddCategories

        public BusinessRuleCollection AddCategories(IEnumerable<string> items)
        {
            return AddCategories(items, null);
        }
        public BusinessRuleCollection AddCategories(IEnumerable<string> items, System.Func<BusinessRule, bool> includeHandler)
        {
            foreach (var item in this)
            {
                if ((includeHandler == null) || (includeHandler.Invoke(item)))
                    item.AddCategories(items);
            }
            return this;
        }
        public BusinessRuleCollection AddCategories(params string[] items)
        {
            return AddCategories(items.AsEnumerable());
        }

        #endregion
        #region Copy

        public BusinessRuleCollection Copy()
        {
            var items = new BusinessRuleCollection();
            items.BusinessSettings.EnabledBusinessRuleCollectionEvents = BusinessRuleCollectionEvents.None;
            foreach (var item in this)
                items.Add(item.Copy());
            items.BusinessSettings.Import(this.BusinessSettings);
            return items;
        }

        #endregion
        #region HasBusinessRules

        public bool HasBusinessRules(BusinessRuleFilter filter)
        {
            if (filter == null)
                throw new System.ArgumentNullException(nameof(filter));

            return HasBusinessRules(filter, filter.IncludeNotBrokenRules);
        }
        public bool HasBusinessRules(BusinessRuleFilter filter, bool includeNotBrokenRules)
        {
            if (filter == null)
                throw new System.ArgumentNullException(nameof(filter));

            switch (filter.FilterType)
            {
                case BusinessRuleFilterType.HasRuleSeverity:
                    return HasBusinessRulesForSeverities(includeNotBrokenRules, filter.Severities);
                case BusinessRuleFilterType.HasCategory:
                    return HasBusinessRulesForCategories(includeNotBrokenRules, filter.Categories);
                case BusinessRuleFilterType.IsBrokenOrHasRuleSeverity:
                    if ((filter?.Severities == null) || (filter.Severities.Count == 0))
                    {
                        return (from o in this
                                where o.IsBroken
                                select o).Any();
                    }
                    else
                    {
                        return (from o in this
                                where o.IsBroken || filter.Severities.Contains(o.Severity)
                                select o).Any();
                    }
                default:
                    return false;
            }
        }

        #endregion
        #region HasBusinessRulesForCategories

        public bool HasBusinessRulesForCategories(bool includeNotBrokenRules, params string[] categories)
        {
            return HasBusinessRulesForCategories(includeNotBrokenRules, categories.AsEnumerable());
        }
        public bool HasBusinessRulesForCategories(bool includeNotBrokenRules, IEnumerable<string> categories)
        {
            if ((categories == null) || (!categories.Any()))
            {
                return (from o in this
                        where (includeNotBrokenRules || o.IsBroken) && (o.Categories == null || o.Categories.Count == 0)
                        select o).Any();
            }
            var index = categories.ToHashSet(o => o, true);
            foreach (var item in this)
            {
                if ((!includeNotBrokenRules && !item.IsBroken) || (item.Categories == null))
                    continue;
                foreach (var category in item.Categories)
                {
                    if (index.Contains(category))
                        return true;
                }
            }
            return false;
        }

        #endregion
        #region HasBusinessRulesForGroups

        public bool HasBusinessRulesForGroups(bool includeNotBrokenRules, params string[] groups)
        {
            return HasBusinessRulesForGroups(includeNotBrokenRules, groups.AsEnumerable());
        }
        public bool HasBusinessRulesForGroups(bool includeNotBrokenRules, IEnumerable<string> groups)
        {
            if (groups == null)
                return false;
            return (from o in this
                    where (includeNotBrokenRules || o.IsBroken) && groups.Contains(o.RuleGroupName, System.StringComparison.OrdinalIgnoreCase)
                    select o).Any();
        }

        #endregion
        #region HasBusinessRulesForProperties

        public bool HasBusinessRulesForProperties(bool includeNotBrokenRules, params string[] properties)
        {
            return HasBusinessRulesForProperties(includeNotBrokenRules, properties.AsEnumerable());
        }
        public bool HasBusinessRulesForProperties(bool includeNotBrokenRules, IEnumerable<string> properties)
        {
            if ((properties == null) || (!properties.Any()))
            {
                return (from o in this
                        where (includeNotBrokenRules || o.IsBroken) && (o.AffectedProperties == null || o.AffectedProperties.Count == 0)
                        select o).Any();
            }
            var index = properties.ToHashSet(o => o, true);
            foreach (var item in this)
            {
                if ((!includeNotBrokenRules && !item.IsBroken) || (item.AffectedProperties == null))
                    continue;
                foreach (var affectedProperty in item.AffectedProperties)
                {
                    if (index.Contains(affectedProperty))
                        return true;
                }
            }
            return false;
        }

        #endregion
        #region HasBusinessRulesForSeverities

        public bool HasBusinessRulesForSeverities(bool includeNotBrokenRules, params RuleSeverity[] severities)
        {
            return HasBusinessRulesForSeverities(includeNotBrokenRules, severities.AsEnumerable());
        }
        public bool HasBusinessRulesForSeverities(bool includeNotBrokenRules, IEnumerable<RuleSeverity> severities)
        {
            if (severities == null)
                return false;
            return (from o in this
                    where (includeNotBrokenRules || o.IsBroken) && severities.Contains(o.Severity)
                    select o).Any();
        }

        #endregion
        #region FindBusinessRules

        public IEnumerable<BusinessRule> FindBusinessRules(BusinessRuleFilter filter)
        {
            if (filter == null)
                throw new System.ArgumentNullException(nameof(filter));
            return FindBusinessRules(filter, filter.IncludeNotBrokenRules);
        }
        public IEnumerable<BusinessRule> FindBusinessRules(BusinessRuleFilter filter, bool includeNotBrokenRules)
        {
            if (filter == null)
                throw new System.ArgumentNullException(nameof(filter));
            switch (filter.FilterType)
            {
                case BusinessRuleFilterType.HasRuleSeverity:
                    return FindBusinessRulesForSeverities(includeNotBrokenRules, filter.Severities);
                case BusinessRuleFilterType.HasCategory:
                    return FindBusinessRulesForCategories(includeNotBrokenRules, filter.Categories);
                case BusinessRuleFilterType.IsBrokenOrHasRuleSeverity:
                    if ((filter?.Severities == null) || (filter.Severities.Count == 0))
                    {
                        return (from o in this
                                where o.IsBroken
                                select o);
                    }
                    else
                    {
                        return (from o in this
                                where o.IsBroken || filter.Severities.Contains(o.Severity)
                                select o);
                    }
                default:
                    return this;
            }
        }

        #endregion
        #region FindBusinessRulesForCategories

        public IEnumerable<BusinessRule> FindBusinessRulesForCategories(bool includeNotBrokenRules, params string[] categories)
        {
            return FindBusinessRulesForCategories(includeNotBrokenRules, categories.AsEnumerable());
        }
        public IEnumerable<BusinessRule> FindBusinessRulesForCategories(bool includeNotBrokenRules, IEnumerable<string> categories)
        {
            if ((categories == null) || (!categories.Any()))
            {
                return (from o in this
                        where (includeNotBrokenRules || o.IsBroken) && (o.Categories == null || o.Categories.Count == 0)
                        select o);
            }
            var index = categories.ToHashSet(o => o, true);
            var items = new List<BusinessRule>();
            foreach (var item in this)
            {
                if ((!includeNotBrokenRules && !item.IsBroken) || (item.Categories == null))
                    continue;
                foreach (var category in item.Categories)
                {
                    if (index.Contains(category))
                    {
                        items.Add(item);
                        break;
                    }
                }
            }
            return items;
        }

        #endregion
        #region FindBusinessRulesForGroups

        public IEnumerable<BusinessRule> FindBusinessRulesForGroups(bool includeNotBrokenRules, params string[] groups)
        {
            return FindBusinessRulesForGroups(includeNotBrokenRules, groups.AsEnumerable());
        }
        public IEnumerable<BusinessRule> FindBusinessRulesForGroups(bool includeNotBrokenRules, IEnumerable<string> groups)
        {
            if (groups == null)
                return new List<BusinessRule>();
            if (!groups.Any())
                return new List<BusinessRule>();
            return (from o in this
                    where (includeNotBrokenRules || o.IsBroken) && groups.Contains(o.RuleGroupName, System.StringComparison.OrdinalIgnoreCase)
                    select o);
        }

        #endregion
        #region FindBusinessRulesForProperties

        public IEnumerable<BusinessRule> FindBusinessRulesForProperties(bool includeNotBrokenRules, params string[] properties)
        {
            return FindBusinessRulesForProperties(includeNotBrokenRules, properties.AsEnumerable());
        }
        public IEnumerable<BusinessRule> FindBusinessRulesForProperties(bool includeNotBrokenRules, IEnumerable<string> properties)
        {
            if ((properties == null) || (!properties.Any()))
            {
                return (from o in this
                        where (includeNotBrokenRules || o.IsBroken) && (o.AffectedProperties == null || o.AffectedProperties.Count == 0)
                        select o);
            }
            var index = properties.ToHashSet(o => o, true);
            var items = new List<BusinessRule>();
            foreach (var item in this)
            {
                if ((!includeNotBrokenRules && !item.IsBroken) || (item.AffectedProperties == null))
                    continue;
                foreach (var affectedProperty in item.AffectedProperties)
                {
                    if (index.Contains(affectedProperty))
                    {
                        items.Add(item);
                        break;
                    }
                }
            }
            return items;
        }

        #endregion
        #region FindBusinessRulesForSeverities

        public IEnumerable<BusinessRule> FindBusinessRulesForSeverities(bool includeNotBrokenRules, params RuleSeverity[] severities)
        {
            return FindBusinessRulesForSeverities(includeNotBrokenRules, severities.AsEnumerable());
        }
        public IEnumerable<BusinessRule> FindBusinessRulesForSeverities(bool includeNotBrokenRules, IEnumerable<RuleSeverity> severities)
        {
            if (severities == null)
                return new List<BusinessRule>();
            if (!severities.Any())
                return new List<BusinessRule>();
            return (from o in this
                    where (includeNotBrokenRules || o.IsBroken) && severities.Contains(o.Severity)
                    select o);
        }

        #endregion
        #region FindAffectedProperties

        public IEnumerable<string> FindAffectedProperties()
        {
            var propertyNames = new HashSet<string>();
            foreach (var item in this)
            {
                foreach (var affectedProperty in item.AffectedProperties)
                {
                    if (!propertyNames.Contains(affectedProperty))
                        propertyNames.Add(affectedProperty);
                }
            }
            return propertyNames;
        }

        #endregion
        #region IsValid

        public bool IsValid()
        {
            return IsValid(BusinessRuleFilter.CreateFilter());
        }
        public bool IsValid(BusinessRuleFilter filter)
        {
            return !HasBusinessRules(filter);
        }

        #endregion
        #region Merge

        public bool Merge(bool includeNotBrokenRules, params BusinessRule[] items)
        {
            return Merge(includeNotBrokenRules, items.AsEnumerable());
        }
        public bool Merge(bool includeNotBrokenRules, IEnumerable<BusinessRule> items)
        {
            bool updated;

            // Merge
            var propertyNames = new HashSet<string>();
            var addedItems = new List<BusinessRule>();
            var removedItems = new List<BusinessRule>();
            using (var manager = new BusinessRuleCollectionSettingsManager(this, BusinessRuleCollectionSettings.CreateSettingsNone()))
            {
                if (!includeNotBrokenRules)
                {
                    items = (from o in items
                             where o != null && o.IsBroken
                             select o);
                }
                else
                {
                    items = (from o in items
                             where o != null
                             select o);
                }
                updated = this.Merge(items,
                   o => GetKey(o),
                   (destinationItem, sourceItem) =>
                   {
                       if (destinationItem.Merge(sourceItem))
                       {
                           ImportPropertyNames(propertyNames, destinationItem);
                           return true;
                       }
                       return false;
                   },
                   sourceItem =>
                   {
                       var sourceItemCopy = sourceItem.Copy();
                       Add(sourceItemCopy);
                       ImportPropertyNames(propertyNames, sourceItemCopy);
                       addedItems.Add(sourceItemCopy);
                   },
                   deletedItem =>
                   {
                       Remove(deletedItem);
                       ImportPropertyNames(propertyNames, deletedItem);
                       removedItems.Add(deletedItem);
                   }, true, true);
            }

            // Trigger events
            if (addedItems.Count > 0)
            {
                if (removedItems.Count > 0)
                    OnCollectionChanged(new System.Collections.Specialized.NotifyCollectionChangedEventArgs(System.Collections.Specialized.NotifyCollectionChangedAction.Reset));
                else
                    OnCollectionChanged(new System.Collections.Specialized.NotifyCollectionChangedEventArgs(System.Collections.Specialized.NotifyCollectionChangedAction.Add, addedItems));
            }
            else if (removedItems.Count > 0)
            {
                OnCollectionChanged(new System.Collections.Specialized.NotifyCollectionChangedEventArgs(System.Collections.Specialized.NotifyCollectionChangedAction.Remove, removedItems));
            }
            OnBusinessRulePropertyChanged(propertyNames);

            return updated;
        }

        #endregion
        #region ToString

        public override string ToString()
        {
            return ToString(System.Environment.NewLine);
        }
        public string ToString(string separator)
        {
            return ToString(separator, BusinessRuleDisplayPositionPolicy.None);
        }
        public string ToString(string separator, BusinessRuleDisplayPositionPolicy displayPositionPolicy)
        {
            return ToString(this, separator, displayPositionPolicy);
        }

        public string ToString(BusinessRuleFilter filter)
        {
            return ToString(filter, System.Environment.NewLine);
        }
        public string ToString(BusinessRuleFilter filter, string separator)
        {
            return ToString(filter, separator, BusinessRuleDisplayPositionPolicy.None);
        }
        public string ToString(BusinessRuleFilter filter, string separator, BusinessRuleDisplayPositionPolicy displayPositionPolicy)
        {
            var items = FindBusinessRules(filter);
            return ToString(items, separator, displayPositionPolicy);
        }

        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity")]
        private static string ToString(IEnumerable<BusinessRule> items, string separator, BusinessRuleDisplayPositionPolicy displayPositionPolicy)
        {
            if (items == null)
                return null;
            switch (displayPositionPolicy)
            {
                case BusinessRuleDisplayPositionPolicy.None:
                    break;
                case BusinessRuleDisplayPositionPolicy.RuleGroupName:
                    items = (from o in items
                             orderby o.RuleGroupName
                             select o);
                    break;
                case BusinessRuleDisplayPositionPolicy.RuleName:
                    items = (from o in items
                             orderby o.RuleName
                             select o);
                    break;
                case BusinessRuleDisplayPositionPolicy.Description:
                    items = (from o in items
                             orderby o.Description
                             select o);
                    break;
                case BusinessRuleDisplayPositionPolicy.Priority:
                    items = (from o in items
                             orderby o.Priority
                             select o);
                    break;
                case BusinessRuleDisplayPositionPolicy.PriorityRuleGroupName:
                    items = (from o in items
                             orderby o.Priority, o.RuleGroupName
                             select o);
                    break;
                case BusinessRuleDisplayPositionPolicy.PriorityRuleName:
                    items = (from o in items
                             orderby o.Priority, o.RuleName
                             select o);
                    break;
                case BusinessRuleDisplayPositionPolicy.PriorityDescription:
                    items = (from o in items
                             orderby o.Priority, o.Description
                             select o);
                    break;
                default:
                    throw new System.ArgumentException("Invalid displayPositionPolicy");
            }
            return items.JoinToString(separator);
        }

        #endregion

        // Protected
        #region BusinessSettings

        private System.Lazy<BusinessRuleCollectionSettings> _BusinessSettings = new System.Lazy<BusinessRuleCollectionSettings>(BusinessRuleCollectionSettings.CreateSettingsAll);
        protected BusinessRuleCollectionSettings BusinessSettings
        {
            get
            {
                return _BusinessSettings.Value;
            }
        }

        #endregion
        #region ClearItems

        protected override void ClearItems()
        {
            var propertyNames = FindAffectedProperties();
            base.ClearItems();

            // Trigger events
            OnCollectionChanged(new System.Collections.Specialized.NotifyCollectionChangedEventArgs(System.Collections.Specialized.NotifyCollectionChangedAction.Reset));
            OnBusinessRulePropertyChanged(propertyNames);
        }

        #endregion
        #region GetKeyForItem

        protected override string GetKeyForItem(BusinessRule item)
        {
            if (item == null)
                throw new System.ArgumentNullException(nameof(item));
            return GetKey(item);
        }

        #endregion
        #region InsertItem

        protected override void InsertItem(int index, BusinessRule item)
        {
            base.InsertItem(index, item);
            OnCollectionChanged(new System.Collections.Specialized.NotifyCollectionChangedEventArgs(System.Collections.Specialized.NotifyCollectionChangedAction.Add, item, index));
        }

        #endregion
        #region RemoveItem

        protected override void RemoveItem(int index)
        {
            base.RemoveItem(index);
            OnCollectionChanged(new System.Collections.Specialized.NotifyCollectionChangedEventArgs(System.Collections.Specialized.NotifyCollectionChangedAction.Remove, index));
        }

        #endregion
        #region OnBusinessRulePropertyChanged

        protected void OnBusinessRulePropertyChanged(IEnumerable<string> propertyNames)
        {
            if (propertyNames == null)
                return;
            if (!propertyNames.Any())
                return;
            OnBusinessRulePropertyChanged(new BusinessRulePropertyEventArgs(propertyNames));
        }
        protected void OnBusinessRulePropertyChanged(BusinessRulePropertyEventArgs args)
        {
            if (this.BusinessSettings.EnabledBusinessRuleCollectionEvents.HasFlag(BusinessRuleCollectionEvents.NotifyBusinessRulePropertyChanged))
            {
                if (this.BusinessRulePropertyChanged != null)
                    this.BusinessRulePropertyChanged.Invoke(this, args);
            }
        }

        #endregion
        #region OnCollectionChanged

        protected void OnCollectionChanged(System.Collections.Specialized.NotifyCollectionChangedEventArgs args)
        {
            if (this.BusinessSettings.EnabledBusinessRuleCollectionEvents.HasFlag(BusinessRuleCollectionEvents.NotifyCollectionChanged))
            {
                if (this.CollectionChanged != null)
                    this.CollectionChanged.Invoke(this, args);
            }
        }

        #endregion

        #region IBusinessRuleCollectionSettingsProvider Members

        BusinessRuleCollectionSettings IBusinessRuleCollectionSettingsProvider.BusinessSettings
        {
            get
            {
                return this.BusinessSettings;
            }
        }

        #endregion

        #endregion
        #region Private Members

        #region ImportPropertyNames

        private static void ImportPropertyNames(HashSet<string> propertyNames, BusinessRule rule)
        {
            if (rule.AffectedProperties == null)
                return;
            foreach (var property in rule.AffectedProperties)
            {
                if (propertyNames.Contains(property))
                    continue;
                propertyNames.Add(property);
            }
        }

        #endregion
        #region GetKey

        private static string GetKey(BusinessRule item)
        {
            if (item == null)
                return null;
            return GetKey(item.RuleGroupName, item.RuleName);
        }
        private static string GetKey(string ruleGroupName, string ruleName)
        {
            if (string.IsNullOrWhiteSpace(ruleGroupName))
                return null;
            if (string.IsNullOrWhiteSpace(ruleName))
                return null;
            return ruleGroupName + "." + ruleName;
        }

        #endregion

        #endregion
        #region Constructors

        public BusinessRuleCollection()
        {
        }

        #endregion
    }
}
